python 121 lines · 3 tabs

Topological Task Scheduling With Cycle Detection and Parallel Waves

Shared by codesnips Sep 2026
3 tabs
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Set


class CycleError(Exception):
    def __init__(self, nodes):
        self.nodes = nodes
        super().__init__(f"dependency cycle detected among: {sorted(nodes)}")


@dataclass
class Task:
    name: str
    run: Callable
    dependencies: Set[str] = field(default_factory=set)


class TaskGraph:
    def __init__(self):
        self._tasks: Dict[str, Task] = {}

    def add_task(self, name, run, depends_on=()):
        deps = set(depends_on)
        missing = deps - set(self._tasks)
        if missing:
            raise KeyError(f"{name} depends on undefined tasks: {sorted(missing)}")
        self._tasks[name] = Task(name=name, run=run, dependencies=deps)
        return self

    def _indegrees(self):
        indegree = {name: len(t.dependencies) for name, t in self._tasks.items()}
        dependents = defaultdict(set)
        for name, task in self._tasks.items():
            for dep in task.dependencies:
                dependents[dep].add(name)
        return indegree, dependents

    def topological_waves(self) -> List[List[str]]:
        indegree, dependents = self._indegrees()
        waves: List[List[str]] = []
        remaining = dict(indegree)

        while remaining:
            ready = sorted(n for n, d in remaining.items() if d == 0)
            if not ready:
                raise CycleError(set(remaining))
            waves.append(ready)
            for node in ready:
                del remaining[node]
                for child in dependents[node]:
                    if child in remaining:
                        remaining[child] -= 1
        return waves

    def task(self, name) -> Task:
        return self._tasks[name]
3 files · python Explain with highlit

This snippet models a workflow as a directed acyclic graph (DAG) of tasks and computes a safe execution order using Kahn's algorithm, then runs independent tasks concurrently. The core idea of topological sorting is that if task B depends on task A, then A must appear before B in any valid ordering; a topological sort produces exactly such an ordering, and the absence of one signals a cycle in the dependency graph.

In taskgraph.py, the TaskGraph stores each Task by name and tracks dependencies per node. add_task registers a node and validates that referenced dependencies are declared, which catches typos early rather than at run time. The _indegrees helper computes how many unmet dependencies each task has — the in-degree in graph terms. This is the input Kahn's algorithm consumes.

The method topological_waves is the heart of the design. Instead of returning a flat list, it groups tasks into waves: each wave is a set of tasks whose dependencies are all already satisfied, so every task in a wave can run in parallel. The algorithm repeatedly collects all zero-indegree nodes as one wave, removes them, and decrements the in-degree of their dependents. When no more zero-indegree nodes remain but tasks are still pending, that residue is a cycle, and CycleError is raised with the offending nodes — a far more useful failure than an infinite loop or silent stall. Sorting node names keeps output deterministic, which matters for tests and reproducibility.

In runner.py, TaskRunner.run walks the waves in order and dispatches each wave to a ThreadPoolExecutor, blocking until the whole wave completes before starting the next. This preserves the dependency guarantee while extracting maximum parallelism at each level. Results are threaded through a shared context dict so downstream tasks can read upstream outputs.

The trade-off is that wave-based scheduling can under-utilize workers when waves are uneven; a fully event-driven scheduler that starts a task the instant its parents finish squeezes out more parallelism at the cost of complexity. For most batch pipelines and build systems, wave scheduling is simpler and good enough. A developer reaches for this pattern whenever ordered, dependency-constrained work must run reliably — CI builds, data pipelines, or migration orchestration.


Related snips

Share this code

Here's the card — post it anywhere.

Topological Task Scheduling With Cycle Detection and Parallel Waves — share card
Link copied