HardMLOps

ETL Dependency Orchestration

MLOps

Hard

Problem

Schedule ETL tasks that form a dependency graph under a shared resource budget. Each task has a unique name, positive integer duration, resource requirement, and depends_on list.

At each time value, first complete every running task whose end time has been reached. Next, collect unscheduled tasks whose dependencies are complete, sort them alphabetically, and greedily start each task that fits the remaining resource budget. Skipped tasks may start at a later completion event. Advance time to the next running-task completion.

Return a list of {"task_name": str, "start_time": int} dictionaries sorted by start time and then task name.

Theory

Real-world data pipelines consist of many tasks with complex interdependencies. A feature engineering task cannot start until the raw data is loaded. A model training task cannot start until features are computed. A reporting task cannot start until all upstream tables are populated.

These dependencies form a Directed Acyclic Graph (DAG) where:

Simply running tasks sequentially is wasteful. If tasks A and B have no dependencies on each other, they could run in parallel, finishing faster overall.

But parallelization is constrained by resources. Each task requires computational resources (CPU, memory, workers). The system has a limited resource budget. You cannot run more tasks concurrently than the budget allows.

An orchestrator must schedule tasks to:

  1. Respect all dependency constraints (never start a task before its prerequisites complete)
  2. Never exceed the resource budget at any point in time
  3. Maximize parallelization to minimize total execution time

DAG Representation

A task graph is typically represented as:

Task Definition:

Example DAG:

This creates a diamond-shaped dependency structure where "train_model" cannot start until both "clean_data" AND "compute_features" finish.


Task States

Throughout execution, each task is in one of four states:

Waiting: The task has at least one incomplete dependency. It cannot start yet.

Ready: All dependencies are complete, but the task has not started (possibly due to resource constraints or tie-breaking).

Running: The task is currently executing. It occupies resources and will complete after its duration.

Completed: The task has finished. Its resources are released, and dependent tasks can now potentially become ready.


The Scheduling Algorithm

At each point in time, the orchestrator follows this process:

Step 1: Complete Finished Tasks

Check all running tasks. Any task whose end time has been reached is marked as completed, and its resources are released.

Step 2: Identify Ready Tasks

A task is ready if:

Step 3: Sort Ready Tasks

Sort the ready tasks alphabetically by name. This deterministic ordering ensures reproducible schedules and breaks ties consistently.

Step 4: Greedily Assign Tasks

Iterate through sorted ready tasks. For each task:

This is a greedy approach: tasks are assigned in alphabetical order as long as resources allow. A large task that does not fit does not block smaller tasks that might fit.

Step 5: Advance Time

If no tasks can be assigned and tasks are still running, advance time to the next task completion event (the earliest end time among running tasks). Then repeat from Step 1.

Step 6: Termination

When all tasks are completed, output the schedule.


Resource Management

Resources are managed as a simple numeric budget:

\text{current\_usage} = \sum_{t \in \text{running}} \text{resources}(t)

A task t can start if:

\text{current\_usage} + \text{resources}(t) \leq \text{budget}

When a task completes, its resources are returned to the pool:

\text{current\_usage} \leftarrow \text{current\_usage} - \text{resources}(t)

This is a simplified model. Real systems may have multiple resource dimensions (CPU, memory, network) and more complex constraints.


Time Advancement

Time does not advance one unit at a time. Instead, the algorithm jumps directly to the next meaningful event: when a running task completes.

\text{next\_time} = \min_{t \in \text{running}} (\text{start\_time}(t) + \text{duration}(t))

This event-driven approach is efficient even for tasks with long durations.

If multiple tasks complete at the same time, process all completions before attempting to start new tasks.


A Detailed Worked Example

Task Definitions:

Resource Budget: 3

Time 0:

Time 2 (A completes):

Time 3 (B completes):

Time 4 (C and D complete):

Final Schedule (sorted by start_time, then name):

Total Makespan: 4 time units

If we had run everything sequentially: 2+3+2+1 = 8 time units. Parallelization saved 50% of the time.


Handling Resource Constraints

Consider a scenario where resources limit parallelism:

Tasks:

Budget: 4

Time 0:

Even though Y is alphabetically before Z in some sense, we process in order X, Y, Z. X fits, Y does not, but we continue and Z fits.

Time 1:

Schedule:


Why Alphabetical Ordering?

Using alphabetical order to break ties ensures:

Determinism: The same input always produces the same schedule, which is essential for debugging and reproducibility.

Simplicity: No need for complex priority schemes or heuristics.

Consistency: Easy to predict and verify the behavior.

In practice, production schedulers may use more sophisticated priority schemes (shortest job first, critical path, etc.), but alphabetical ordering provides a clear baseline.


Complexity Analysis

Let n be the number of tasks and m be the total number of dependency edges.

Building the graph: O(n + m)

Processing events: At most n completion events, each involving sorting ready tasks (O(n \log n)) and checking resource constraints (O(n)).

Overall: O(n^2 \log n) in the worst case, though typically much faster in practice with sparse dependencies.


Output Format

The schedule is returned as a list of tuples:

[(\text{task\_name}, \text{start\_time}), ...]

Sorted first by start_time (ascending), then by task_name (alphabetically) for ties.

This format allows easy verification that:


Where Task Orchestration Shows Up

Apache Airflow: The most popular open-source workflow orchestrator, using DAGs to define ETL pipelines.

Prefect: Modern Python-native workflow orchestration with dynamic task graphs.

Luigi: Spotify's pipeline framework with automatic dependency resolution.

dbt: Data transformation tool that builds DAGs from SQL dependencies.

Kubernetes Jobs: Container orchestration with dependency-based job scheduling.

CI/CD Pipelines: Build systems (Jenkins, GitHub Actions, GitLab CI) schedule jobs with dependencies.

MapReduce/Spark: Big data frameworks manage task dependencies within computation graphs.

Understanding task orchestration is fundamental to building reliable, efficient data pipelines at scale.

Examples

Example 1

Input
tasks = [{"name": "extract", "duration": 2, "resources": 1, "depends_on": []}, {"name": "transform", "duration": 3, "resources": 1, "depends_on": ["extract"]}, {"name": "load", "duration": 1, "resources": 1, "depends_on": ["transform"]}], resource_budget = 2
Output
[{"task_name": "extract", "start_time": 0}, {"task_name": "transform", "start_time": 2}, {"task_name": "load", "start_time": 5}]
Explanation
Each task starts when its preceding dependency finishes.

Example 2

Input
tasks = [{"name": "fetch_orders", "duration": 3, "resources": 1, "depends_on": []}, {"name": "fetch_users", "duration": 2, "resources": 1, "depends_on": []}, {"name": "join", "duration": 1, "resources": 2, "depends_on": ["fetch_users", "fetch_orders"]}], resource_budget = 2
Output
[{"task_name": "fetch_orders", "start_time": 0}, {"task_name": "fetch_users", "start_time": 0}, {"task_name": "join", "start_time": 3}]

Hints

  1. Track running tasks as {name: end_time} and advance to min(running.values()).
  2. After completions, sort ready task dictionaries with key=lambda task: task["name"].

Requirements

Constraints

Starter Code

def schedule_pipeline(tasks: list, resource_budget: int) -> list:
    """
    Returns a list of schedule dictionaries.
    """
    # Write code here
    pass

Test Cases

CaseMatches
linear chainpublic
parallel mergepublic