java-topology/defects/kafka/unit/KafkaStreamsAssignTasksThreadsTest.java

223 lines
9.3 KiB
Java
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.

package unit;
import java.util.*;
/**
* kafka-0007: StreamsPartitionAssignor.assignTasksToThreads()
* PriorityQueue.contains() O(T²) → HashSet O(T)
*
* Demonstrates that PriorityQueue.contains() is O(N) (linear scan of heap array),
* making the nested task-assignment loop O(C × T²) instead of O(C × T).
*
* Compile: javac -d . KafkaStreamsAssignTasksThreadsTest.java
* Run: java unit.KafkaStreamsAssignTasksThreadsTest
*/
public class KafkaStreamsAssignTasksThreadsTest {
static class TaskId implements Comparable<TaskId> {
final int id;
TaskId(int id) { this.id = id; }
@Override public int compareTo(TaskId o) { return Integer.compare(this.id, o.id); }
@Override public boolean equals(Object o) { return o instanceof TaskId && ((TaskId)o).id == id; }
@Override public int hashCode() { return Integer.hashCode(id); }
@Override public String toString() { return "T" + id; }
}
// -------------------------------------------------------------------
// DEFECTIVE: simulates the double-loop pattern in assignTasksToThreads
// PriorityQueue.contains() is O(T) — linear scan of backing heap array
// consumersToFill (LinkedList) .contains is O(C)
// -------------------------------------------------------------------
static long defectiveAssign(List<String> consumers, List<TaskId> prevTasksPerConsumer,
List<TaskId> allTasks) {
long ops = 0;
final PriorityQueue<TaskId> unassigned = new PriorityQueue<>(allTasks);
final Queue<String> consumersToFill = new LinkedList<>();
final Map<TaskId, String> skipped = new TreeMap<>();
// First pass: assign to previous owners
for (String consumer : consumers) {
for (TaskId task : prevTasksPerConsumer) {
ops++; // loop iteration
// PriorityQueue.contains is O(T) — count as T ops
ops += unassigned.size(); // simulate O(T) linear scan
if (unassigned.contains(task)) {
unassigned.remove(task);
} else {
skipped.put(task, consumer);
}
}
consumersToFill.offer(consumer);
}
// Second pass: skipped tasks
for (Map.Entry<TaskId, String> e : skipped.entrySet()) {
TaskId task = e.getKey();
String consumer = e.getValue();
// LinkedList.contains is O(C) — count as C ops
ops += consumersToFill.size(); // simulate O(C) scan
ops += unassigned.size(); // simulate O(T) scan
if (consumersToFill.contains(consumer) && unassigned.contains(task)) {
unassigned.remove(task);
consumersToFill.remove(consumer);
}
}
return ops;
}
// -------------------------------------------------------------------
// FIXED: HashSet for O(1) membership, LinkedHashSet for consumer fill
// -------------------------------------------------------------------
static long fixedAssign(List<String> consumers, List<TaskId> prevTasksPerConsumer,
List<TaskId> allTasks) {
long ops = 0;
final PriorityQueue<TaskId> unassignedOrdered = new PriorityQueue<>(allTasks);
final Set<TaskId> unassignedSet = new HashSet<>(allTasks);
final Set<String> consumersToFill = new LinkedHashSet<>();
final Map<TaskId, String> skipped = new TreeMap<>();
// First pass: assign to previous owners
for (String consumer : consumers) {
for (TaskId task : prevTasksPerConsumer) {
ops++; // loop iteration
ops++; // O(1) HashSet.contains
if (unassignedSet.contains(task)) {
unassignedSet.remove(task);
unassignedOrdered.remove(task);
} else {
skipped.put(task, consumer);
}
}
consumersToFill.add(consumer);
}
// Second pass: skipped tasks
for (Map.Entry<TaskId, String> e : skipped.entrySet()) {
TaskId task = e.getKey();
String consumer = e.getValue();
ops++; // O(1) LinkedHashSet.contains
ops++; // O(1) HashSet.contains
if (consumersToFill.contains(consumer) && unassignedSet.contains(task)) {
unassignedSet.remove(task);
unassignedOrdered.remove(task);
consumersToFill.remove(consumer);
}
}
return ops;
}
static void test(String name, boolean condition) {
System.out.println((condition ? "PASS" : "FAIL") + ": " + name);
if (!condition) throw new AssertionError("FAIL: " + name);
}
public static void main(String[] args) {
System.out.println("kafka-0007: StreamsPartitionAssignor.assignTasksToThreads()");
System.out.println(" PriorityQueue.contains() O(T²) → HashSet O(T)");
System.out.println("=".repeat(65));
int[] taskSizes = {50, 100, 200, 400};
int consumers = 5;
boolean allPass = true;
System.out.printf("%-8s %-12s %-12s %-10s%n",
"Tasks(T)", "Defective-ops", "Fixed-ops", "Ratio");
System.out.println("-".repeat(50));
long prevSlowOps = -1;
long prevFastOps = -1;
for (int T : taskSizes) {
// Build T tasks
List<TaskId> allTasks = new ArrayList<>();
for (int i = 0; i < T; i++) allTasks.add(new TaskId(i));
// Each consumer has T/2 previous tasks (worst case: most tasks are "known")
List<TaskId> prev = allTasks.subList(0, T / 2);
List<String> consumerList = new ArrayList<>();
for (int c = 0; c < consumers; c++) consumerList.add("consumer-" + c);
long slowOps = defectiveAssign(consumerList, prev, allTasks);
long fastOps = fixedAssign(consumerList, prev, allTasks);
double ratio = (double) slowOps / Math.max(fastOps, 1);
System.out.printf("T=%-6d %-12d %-12d %.1fx%n", T, slowOps, fastOps, ratio);
boolean ratioOk = ratio >= 5.0;
if (!ratioOk) allPass = false;
// O(T²) scaling: doubling T should roughly 4x slow ops
if (prevSlowOps > 0) {
double slowScale = (double) slowOps / prevSlowOps;
double fastScale = (double) fastOps / prevFastOps;
boolean superlinear = slowScale >= 3.0;
boolean linear = fastScale <= 2.5;
if (!superlinear || !linear) allPass = false;
}
prevSlowOps = slowOps;
prevFastOps = fastOps;
}
System.out.println("=".repeat(65));
// Explicit PASS/FAIL tests
{
int T = 200;
List<TaskId> allTasks = new ArrayList<>();
for (int i = 0; i < T; i++) allTasks.add(new TaskId(i));
List<TaskId> prev = allTasks.subList(0, T / 2);
List<String> consumerList = new ArrayList<>();
for (int c = 0; c < consumers; c++) consumerList.add("c" + c);
long slowOps = defectiveAssign(consumerList, prev, allTasks);
long fastOps = fixedAssign(consumerList, prev, allTasks);
double ratio = (double) slowOps / Math.max(fastOps, 1);
try {
test("T=200: defective ops >> fixed ops (ratio >= 5x)", ratio >= 5.0);
test("T=200: defective ops are super-linear (>= T*T/4)",
slowOps >= (long) T * T / 4);
test("T=200: fixed ops are linear (< T*T/10)",
fastOps < (long) T * T / 10);
} catch (AssertionError e) {
allPass = false;
System.out.println(e.getMessage());
}
}
// Doubling test
{
int T1 = 100, T2 = 200;
List<String> cs = Arrays.asList("c0","c1","c2","c3","c4");
List<TaskId> tasks1 = new ArrayList<>();
for (int i = 0; i < T1; i++) tasks1.add(new TaskId(i));
List<TaskId> tasks2 = new ArrayList<>();
for (int i = 0; i < T2; i++) tasks2.add(new TaskId(i));
long s1 = defectiveAssign(cs, tasks1.subList(0, T1/2), tasks1);
long s2 = defectiveAssign(cs, tasks2.subList(0, T2/2), tasks2);
long f1 = fixedAssign(cs, tasks1.subList(0, T1/2), tasks1);
long f2 = fixedAssign(cs, tasks2.subList(0, T2/2), tasks2);
double slowScale = (double) s2 / s1;
double fastScale = (double) f2 / f1;
System.out.printf("Doubling T: defective-ops scale=%.2fx (expect ~4x), fixed-ops scale=%.2fx (expect ~2x)%n",
slowScale, fastScale);
try {
test("Doubling T: defective ops scale >= 3x (super-linear, O(T²))", slowScale >= 3.0);
test("Doubling T: fixed ops scale <= 2.5x (linear, O(T))", fastScale <= 2.5);
} catch (AssertionError e) {
allPass = false;
System.out.println(e.getMessage());
}
}
System.out.println("=".repeat(65));
System.out.println(allPass ? "ALL PASS" : "SOME FAILED");
if (!allPass) System.exit(1);
}
}