254 lines
10 KiB
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
|
|
}
|
|
}
|