wave9 complete: 458/207 — airflow/argo/pytorch-0003/tf/jax/pyg/grpc/thrift + graphhopper/valhalla

This commit is contained in:
russell@unturf.com 2026-03-27 16:59:04 -04:00
parent 81bc62b9cb
commit ab10b2f555
22 changed files with 2889 additions and 16 deletions

View file

@ -0,0 +1,130 @@
# argo-0001 — O(N) Linear Scan in GetTask() Called Inside DAG Execution Loop
**Severity:** HIGH
**Complexity:** O(N·D) → O(D) per execution cycle, overall O(N²) → O(N)
**CWE:** CWE-407 (Algorithmic Complexity)
## Affected File
`workflow/controller/dag.go` lines 83–89
## Defective Code
```go
// dagContext holds context information about this context's DAG
type dagContext struct {
// tasks are all the tasks in the template
tasks []wfv1.DAGTask // <-- stored as a SLICE
...
}
// GetTask returns the DAGTask for the given taskName.
// Called from: resolveDependencies, assessDAGPhase, executeDAG task loop,
// ancestry.go GetTaskDependencies, validate.go
func (d *dagContext) GetTask(ctx context.Context, taskName string) *wfv1.DAGTask {
for _, task := range d.tasks { // O(N) linear scan
if task.Name == taskName {
return &task
}
}
panic("target " + taskName + " does not exist")
}
```
**Why it is O(N²):** `GetTask()` is called multiple times per task during
`executeDAG()`, which itself iterates over all tasks (or target tasks). The
outer loop runs once per task being executed. Each call to `GetTask()` scans
the entire `tasks` slice. For a DAG with N tasks, this yields O(N²) total
comparisons per reconciliation cycle.
### Call graph (defective paths)
```
executeDAG()
└── for _, taskName := range targetTasks [O(N) tasks]
├── GetTask(taskName) [O(N) each]
├── GetTask(taskName).Hooks → GetTask(taskName) [O(N) each]
└── GetTask(taskName).GetExitHook(...) [O(N) each]
assessDAGPhase()
└── for _, depName := range targetTasks [O(N)]
└── GetTask(depName) [O(N) each]
ancestry.go:GetTaskAncestry()
└── recursive DFS over all ancestors
└── GetTaskDependencies → resolveDependencies
└── GetTask(taskName) [O(N) each]
ancestry.go:getTaskDependsLogic()
└── for _, dependency := range dagTask.Dependencies [O(D)]
└── GetTask(dependency) [O(N) each]
```
Note: `dagValidationContext.GetTask()` in `validate.go:1315` correctly uses a
`map[string]wfv1.DAGTask` for O(1) lookup. The runtime `dagContext` uses a
slice — this discrepancy is the defect.
## Fixed Code
### Step 1: Add a task lookup map to `dagContext`
```go
type dagContext struct {
boundaryName string
boundaryID string
tasks []wfv1.DAGTask
taskByName map[string]*wfv1.DAGTask // <-- add this field
...
}
```
### Step 2: Populate the map in `executeDAG` where dagContext is constructed
```go
dagCtx := &dagContext{
boundaryName: nodeName,
boundaryID: node.ID,
tasks: tmpl.DAG.Tasks,
visited: make(map[string]bool),
tmpl: tmpl,
wf: woc.wf,
tmplCtx: tmplCtx,
dependencies: make(map[string][]string),
dependsLogic: make(map[string]string),
log: woc.log,
}
// Build O(1) task lookup map
dagCtx.taskByName = make(map[string]*wfv1.DAGTask, len(tmpl.DAG.Tasks))
for i := range dagCtx.tasks {
dagCtx.taskByName[dagCtx.tasks[i].Name] = &dagCtx.tasks[i]
}
```
### Step 3: Replace GetTask body
```go
func (d *dagContext) GetTask(ctx context.Context, taskName string) *wfv1.DAGTask {
if task, ok := d.taskByName[taskName]; ok {
return task
}
panic("target " + taskName + " does not exist")
}
```
## Complexity Analysis
| Version | GetTask | Per executeDAG cycle | N=500 tasks |
|---------|---------|---------------------|-------------|
| Defective | O(N) | O(N²) | ~125,000 compares |
| Fixed | O(1) | O(N) | ~500 lookups |
For a DAG with 500 tasks, the fixed version performs 250× fewer operations per
reconciliation cycle.
## Trigger Path
Every time Argo's workflow controller reconciles a running DAG workflow, it
calls `executeDAG()` → `executeDAGTask()` for each pending task. With N=500 tasks
and 3 `GetTask()` calls per task in the hot path, each reconciliation does 750×500=375,000
string comparisons instead of 750 map lookups. At 1 reconcile/second for a
long-running workflow, this adds seconds of unnecessary CPU per minute.

View file

@ -0,0 +1,245 @@
package unit;
import java.util.*;
/**
* argo-0001 — O(N) linear GetTask() scan inside O(N) DAG execution loop
*
* Simulates the defective GetTask() from:
* workflow/controller/dag.go:83-89
*
* Defect: dagContext stores tasks as []DAGTask (slice). GetTask(name) does
* linear scan. Called O(N) times per executeDAG() cycle → O(N²) total.
*
* Fix: Build a map[string]*DAGTask at dagContext construction time → O(1) lookup.
*
* The unit test simulates:
* 1. A DAGContext with N tasks (stored as list in defective, map in fixed)
* 2. An executeDAG loop that calls GetTask() once per targetTask
* 3. Counts total comparisons to confirm O(N²) vs O(N)
*/
public class ArgoGetTaskAlgorithm {
static int opCount = 0;
static void check(String desc, boolean cond) {
System.out.println((cond ? "PASS" : "FAIL") + ": " + desc);
if (!cond) throw new AssertionError("FAIL: " + desc);
}
// -----------------------------------------------------------------------
// Simulated DAGTask
// -----------------------------------------------------------------------
static class DAGTask {
final String name;
final List<String> dependencies;
DAGTask(String name, List<String> deps) {
this.name = name;
this.dependencies = deps;
}
}
// -----------------------------------------------------------------------
// DEFECTIVE: tasks stored as List<DAGTask>, GetTask does linear scan
// -----------------------------------------------------------------------
static class DefectiveDagContext {
final List<DAGTask> tasks;
DefectiveDagContext(List<DAGTask> tasks) {
this.tasks = new ArrayList<>(tasks);
}
DAGTask getTask(String taskName) {
for (DAGTask task : tasks) { // O(N) scan
opCount++;
if (task.name.equals(taskName)) {
return task;
}
}
throw new RuntimeException("task not found: " + taskName);
}
// Simulates executeDAG: for each target task, call getTask() 3 times
// (matches the 3 GetTask calls on lines 292, 300, 307 in dag.go)
void executeDAG(List<String> targetTasks) {
for (String taskName : targetTasks) {
getTask(taskName); // line 292: task := dagCtx.GetTask(ctx, taskName)
getTask(taskName); // line 300: dagCtx.GetTask(ctx, taskName).Hooks
getTask(taskName); // line 307: dagCtx.GetTask(ctx, taskName).GetExitHook(...)
}
}
}
// -----------------------------------------------------------------------
// FIXED: tasks stored as Map<String, DAGTask>, GetTask does O(1) lookup
// -----------------------------------------------------------------------
static class FixedDagContext {
final List<DAGTask> tasks;
final Map<String, DAGTask> taskByName;
FixedDagContext(List<DAGTask> tasks) {
this.tasks = new ArrayList<>(tasks);
this.taskByName = new HashMap<>(tasks.size() * 2);
for (DAGTask t : tasks) {
this.taskByName.put(t.name, t);
}
}
DAGTask getTask(String taskName) {
opCount++; // count each lookup
DAGTask task = taskByName.get(taskName);
if (task == null) throw new RuntimeException("task not found: " + taskName);
return task;
}
void executeDAG(List<String> targetTasks) {
for (String taskName : targetTasks) {
getTask(taskName);
getTask(taskName);
getTask(taskName);
}
}
}
// Build N tasks (all as target tasks — worst case: getTask scans entire list)
static List<DAGTask> buildTasks(int n) {
List<DAGTask> tasks = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
tasks.add(new DAGTask("task-" + i, Collections.emptyList()));
}
return tasks;
}
public static void main(String[] args) {
int passed = 0;
int total = 0;
// ------------------------------------------------------------------
// Test 1: Correctness — both return same task
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks = buildTasks(10);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
DAGTask defTask = def.getTask("task-7");
opCount = 0;
DAGTask fixTask = fix.getTask("task-7");
check("Correctness: both find task-7", defTask.name.equals("task-7") && fixTask.name.equals("task-7"));
passed++;
}
// ------------------------------------------------------------------
// Test 2: Op-count for getTask on last element — defective is O(N)
// ------------------------------------------------------------------
total++;
{
int n = 100;
List<DAGTask> tasks = buildTasks(n);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
def.getTask("task-99"); // last element -> worst case: N comparisons
long defOps = opCount;
opCount = 0;
fix.getTask("task-99");
long fixOps = opCount;
System.out.printf(" Single getTask (last of N=%d): defective=%d ops, fixed=%d ops%n",
n, defOps, fixOps);
check("Defective getTask(last) scans all N elements", defOps == n);
check("Fixed getTask is O(1)", fixOps == 1);
passed += 2;
total++;
}
// ------------------------------------------------------------------
// Test 3: executeDAG op-count ratio — O(N²) vs O(N)
// ------------------------------------------------------------------
total++;
{
int n = 300;
List<DAGTask> tasks = buildTasks(n);
List<String> targetTasks = new ArrayList<>();
for (DAGTask t : tasks) targetTasks.add(t.name);
DefectiveDagContext def = new DefectiveDagContext(tasks);
FixedDagContext fix = new FixedDagContext(tasks);
opCount = 0;
def.executeDAG(targetTasks);
long defOps = opCount;
opCount = 0;
fix.executeDAG(targetTasks);
long fixOps = opCount;
double ratio = (double) defOps / fixOps;
System.out.printf(" executeDAG N=%d: defective=%d ops, fixed=%d ops, ratio=%.1fx%n",
n, defOps, fixOps, ratio);
// Defective: 3 calls × avg N/2 scans × N tasks = 3N²/2
// Fixed: 3 calls × 1 lookup × N tasks = 3N
// Expected ratio ≈ N/2 = 150 for N=300; require >= 50x conservatively
check("Op-count ratio >= 50x for executeDAG N=" + n, ratio >= 50.0);
passed++;
}
// ------------------------------------------------------------------
// Test 4: Quadratic growth — ops at N=200 should be ~4x ops at N=100
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks100 = buildTasks(100);
List<DAGTask> tasks200 = buildTasks(200);
List<String> target100 = new ArrayList<>(); for (DAGTask t : tasks100) target100.add(t.name);
List<String> target200 = new ArrayList<>(); for (DAGTask t : tasks200) target200.add(t.name);
opCount = 0;
new DefectiveDagContext(tasks100).executeDAG(target100);
long ops100 = opCount;
opCount = 0;
new DefectiveDagContext(tasks200).executeDAG(target200);
long ops200 = opCount;
double growthRatio = (double) ops200 / ops100;
System.out.printf(" Defective growth (200 vs 100): %.2fx (expected ~4x for O(N²))%n",
growthRatio);
check("Defective shows quadratic growth (ratio >= 3.5x)", growthRatio >= 3.5);
passed++;
}
// ------------------------------------------------------------------
// Test 5: Linear growth for fixed
// ------------------------------------------------------------------
total++;
{
List<DAGTask> tasks100 = buildTasks(100);
List<DAGTask> tasks200 = buildTasks(200);
List<String> target100 = new ArrayList<>(); for (DAGTask t : tasks100) target100.add(t.name);
List<String> target200 = new ArrayList<>(); for (DAGTask t : tasks200) target200.add(t.name);
opCount = 0;
new FixedDagContext(tasks100).executeDAG(target100);
long ops100 = opCount;
opCount = 0;
new FixedDagContext(tasks200).executeDAG(target200);
long ops200 = opCount;
double growthRatio = (double) ops200 / ops100;
System.out.printf(" Fixed growth (200 vs 100): %.2fx (expected ~2x for O(N))%n",
growthRatio);
check("Fixed shows linear growth (ratio < 3.0x)", growthRatio < 3.0);
passed++;
}
System.out.printf("%n%d/%d PASS%n", passed, total);
}
}