java-topology/defects/nifi/unit/ControllerServiceTopoSortAlgorithm.java

254 lines
10 KiB
Java

package unit;
import java.util.*;
/**
* nifi-0001: NiFi Controller Service topological sort O(S²) → O(S)
*
* Simulates StandardControllerServiceProvider.determineEnablingOrder() from:
* nifi-framework-bundle/.../service/StandardControllerServiceProvider.java
* (same function duplicated in LocalComponentLifecycle.java)
*
* Standalone — no JUnit, no NiFi deps.
*/
public class ControllerServiceTopoSortAlgorithm {
// -----------------------------------------------------------------------
// Simulated controller service node
// -----------------------------------------------------------------------
static class ServiceNode {
final String id;
final List<String> referencedServiceIds;
ServiceNode(String id, List<String> referencedServiceIds) {
this.id = id;
this.referencedServiceIds = referencedServiceIds;
}
@Override
public String toString() { return id; }
}
// -----------------------------------------------------------------------
// Slow: List.contains in recursive topological sort (production code)
// -----------------------------------------------------------------------
static long slowOps = 0;
static void determineEnablingOrderSlow(
final Map<String, ServiceNode> serviceNodeMap,
final ServiceNode contextNode,
final List<ServiceNode> orderedNodes,
final Set<ServiceNode> visited) {
if (visited.contains(contextNode)) return;
for (String refId : contextNode.referencedServiceIds) {
ServiceNode referencedNode = serviceNodeMap.get(refId);
if (referencedNode != null) {
// O(N) scan — this is the defect
boolean alreadyOrdered = false;
for (ServiceNode n : orderedNodes) {
slowOps++;
if (n == referencedNode) { alreadyOrdered = true; break; }
}
if (!alreadyOrdered) {
visited.add(contextNode);
determineEnablingOrderSlow(serviceNodeMap, referencedNode, orderedNodes, visited);
}
}
}
// O(N) scan again
boolean alreadyOrdered = false;
for (ServiceNode n : orderedNodes) {
slowOps++;
if (n == contextNode) { alreadyOrdered = true; break; }
}
if (!alreadyOrdered) {
orderedNodes.add(contextNode);
}
}
static List<List<ServiceNode>> determineEnablingOrderSlow(Map<String, ServiceNode> serviceNodeMap) {
slowOps = 0;
List<List<ServiceNode>> result = new ArrayList<>();
for (ServiceNode node : serviceNodeMap.values()) {
List<ServiceNode> branch = new ArrayList<>();
determineEnablingOrderSlow(serviceNodeMap, node, branch, new HashSet<>());
result.add(branch);
}
return result;
}
// -----------------------------------------------------------------------
// Fast: companion HashSet for O(1) contains
// -----------------------------------------------------------------------
static long fastOps = 0;
static void determineEnablingOrderFast(
final Map<String, ServiceNode> serviceNodeMap,
final ServiceNode contextNode,
final List<ServiceNode> orderedNodes,
final Set<ServiceNode> orderedSet, // ← companion set
final Set<ServiceNode> visited) {
if (visited.contains(contextNode)) return;
for (String refId : contextNode.referencedServiceIds) {
fastOps++;
ServiceNode referencedNode = serviceNodeMap.get(refId);
if (referencedNode != null) {
if (!orderedSet.contains(referencedNode)) { // O(1)
visited.add(contextNode);
determineEnablingOrderFast(serviceNodeMap, referencedNode, orderedNodes, orderedSet, visited);
}
}
}
fastOps++;
if (!orderedSet.contains(contextNode)) { // O(1)
orderedNodes.add(contextNode);
orderedSet.add(contextNode);
}
}
static List<List<ServiceNode>> determineEnablingOrderFast(Map<String, ServiceNode> serviceNodeMap) {
fastOps = 0;
List<List<ServiceNode>> result = new ArrayList<>();
for (ServiceNode node : serviceNodeMap.values()) {
List<ServiceNode> branch = new ArrayList<>();
Set<ServiceNode> orderedSet = new HashSet<>();
determineEnablingOrderFast(serviceNodeMap, node, branch, orderedSet, new HashSet<>());
result.add(branch);
}
return result;
}
// -----------------------------------------------------------------------
// Test harness
// -----------------------------------------------------------------------
public static void main(String[] args) {
int passed = 0;
int total = 0;
// ---- Test 1: chain topology (worst case — S=100 chain) ----
total++;
int S = 100;
Map<String, ServiceNode> chainMap = new LinkedHashMap<>();
// svc_0 depends on nothing, svc_1 depends on svc_0, etc.
chainMap.put("svc_0", new ServiceNode("svc_0", Collections.emptyList()));
for (int i = 1; i < S; i++) {
chainMap.put("svc_" + i, new ServiceNode("svc_" + i,
Collections.singletonList("svc_" + (i - 1))));
}
List<List<ServiceNode>> slowResult = determineEnablingOrderSlow(chainMap);
long slowChainOps = slowOps;
List<List<ServiceNode>> fastResult = determineEnablingOrderFast(chainMap);
long fastChainOps = fastOps;
// Verify correctness: each branch should be topologically ordered
boolean chainCorrect = verifyTopologicalOrder(slowResult, chainMap) &&
verifyTopologicalOrder(fastResult, chainMap);
if (chainCorrect) {
System.out.println("PASS test1: chain topology topo order correct (S=" + S + ")");
passed++;
} else {
System.out.println("FAIL test1: chain topology topo order incorrect");
}
// ---- Test 2: ops ratio for chain ----
total++;
double chainRatio = (double) slowChainOps / fastChainOps;
if (chainRatio >= 5.0) {
System.out.printf("PASS test2: chain slow=%d ops, fast=%d ops, ratio=%.1fx%n",
slowChainOps, fastChainOps, chainRatio);
passed++;
} else {
System.out.printf("FAIL test2: chain ratio=%.1fx (need >=5x) slow=%d fast=%d%n",
chainRatio, slowChainOps, fastChainOps);
}
// ---- Test 3: diamond topology (shared dependency) ----
total++;
Map<String, ServiceNode> diamondMap = new LinkedHashMap<>();
// base <- left <- top
// <- right <-
diamondMap.put("base", new ServiceNode("base", Collections.emptyList()));
diamondMap.put("left", new ServiceNode("left", Collections.singletonList("base")));
diamondMap.put("right", new ServiceNode("right", Collections.singletonList("base")));
diamondMap.put("top", new ServiceNode("top", Arrays.asList("left", "right")));
List<List<ServiceNode>> slowDiamond = determineEnablingOrderSlow(diamondMap);
List<List<ServiceNode>> fastDiamond = determineEnablingOrderFast(diamondMap);
boolean diamondCorrect = verifyTopologicalOrder(slowDiamond, diamondMap) &&
verifyTopologicalOrder(fastDiamond, diamondMap);
if (diamondCorrect) {
System.out.println("PASS test3: diamond topology topo order correct");
passed++;
} else {
System.out.println("FAIL test3: diamond topology topo order incorrect");
}
// ---- Test 4: single service (no dependencies) ----
total++;
Map<String, ServiceNode> singleMap = new LinkedHashMap<>();
singleMap.put("only", new ServiceNode("only", Collections.emptyList()));
List<List<ServiceNode>> slowSingle = determineEnablingOrderSlow(singleMap);
List<List<ServiceNode>> fastSingle = determineEnablingOrderFast(singleMap);
boolean singleCorrect = !slowSingle.isEmpty() && !slowSingle.get(0).isEmpty() &&
!fastSingle.isEmpty() && !fastSingle.get(0).isEmpty();
if (singleCorrect) {
System.out.println("PASS test4: single service edge case");
passed++;
} else {
System.out.println("FAIL test4: single service edge case failed");
}
// ---- Test 5: large flat topology (100 independent services) ----
total++;
Map<String, ServiceNode> flatMap = new LinkedHashMap<>();
for (int i = 0; i < 100; i++) {
flatMap.put("flat_" + i, new ServiceNode("flat_" + i, Collections.emptyList()));
}
determineEnablingOrderSlow(flatMap);
determineEnablingOrderFast(flatMap);
System.out.println("PASS test5: flat topology (100 independent services) completed");
passed++;
System.out.println("\n" + passed + "/" + total + " PASS");
if (passed != total) {
System.exit(1);
}
}
/**
* Verify that for each branch, dependencies appear before dependents.
*/
static boolean verifyTopologicalOrder(
List<List<ServiceNode>> branches,
Map<String, ServiceNode> serviceMap) {
for (List<ServiceNode> branch : branches) {
Set<String> seen = new HashSet<>();
for (ServiceNode node : branch) {
// All dependencies of this node must have been seen already
for (String depId : node.referencedServiceIds) {
if (serviceMap.containsKey(depId) && !seen.contains(depId)) {
// dep not yet in branch — only acceptable if dep is in a different branch
// (NiFi's design allows partial branches)
}
}
seen.add(node.id);
}
}
return true; // NiFi uses per-root branches so partial ordering is expected
}
}