Implement Dependency-Aware Task Scheduler
Scale AISoftware Engineer · Technical ScreenLast updated
Design and implement a dependency-aware task scheduler that supports dynamic task registration, cycle detection, priority-based ready task retrieval, and state transitions.
Problem Statement
In distributed annotation and model training pipelines, tasks often have inter-dependencies and variable priorities. You need to design an in-memory task scheduler that manages tasks with dependencies, resolves execution readiness dynamically, and dispatches tasks based on priority.
Implement the TaskScheduler class with the following methods:
__init__(): Initializes the scheduler.add_task(task_id: str, priority: int, dependencies: list[str]) -> bool: Registers a new task with an integerpriority(higher value indicates higher priority) and a list ofdependencies(task IDs that must be completed beforetask_idcan execute). Dependencies may refer to tasks that have not yet been registered or tasks already completed.- If
task_idalready exists, returnFalsewithout making changes. - If adding this task introduces a circular dependency among currently known tasks, return
Falsewithout modifying the scheduler state. - Otherwise, register the task in state
PENDINGand returnTrue.
- If
get_next_task() -> str | None: Selects and returns thetask_idof the ready task with the highest priority.- A task is considered ready if it is in the
PENDINGstate and all its dependencies are in theCOMPLETEDstate (dependencies that have never been registered are considered incomplete). - If multiple ready tasks have the same highest priority, break ties by selecting the one with the lexicographically smallest
task_id. - Upon being returned, the task transitions from
PENDINGtoIN_PROGRESS. - If no task is ready, return
None.
- A task is considered ready if it is in the
complete_task(task_id: str) -> bool: Transitions a task fromIN_PROGRESStoCOMPLETED.- Return
Trueiftask_idwas inIN_PROGRESSand successfully transitions toCOMPLETED. - Return
Falseiftask_iddoes not exist or was not currentlyIN_PROGRESS. - Completing a task should unblock any dependent tasks whose remaining prerequisite tasks are all completed.
- Return
Method Signatures
class TaskScheduler:
def __init__(self):
pass
def add_task(self, task_id: str, priority: int, dependencies: list[str]) -> bool:
pass
def get_next_task(self) -> str | None:
pass
def complete_task(self, task_id: str) -> bool:
pass
Examples
Example 1
scheduler = TaskScheduler()
scheduler.add_task("B", 10, ["A"]) # returns True; A is not yet added
scheduler.get_next_task() # returns None (B is blocked by uncompleted A)
scheduler.add_task("A", 5, []) # returns True; A has no dependencies
scheduler.get_next_task() # returns "A" (A is ready; state -> IN_PROGRESS)
scheduler.complete_task("A") # returns True (A -> COMPLETED; B is now ready)
scheduler.get_next_task() # returns "B" (B is now ready; state -> IN_PROGRESS)
scheduler.complete_task("B") # returns True
Example 2 (Cycle Detection)
scheduler = TaskScheduler()
scheduler.add_task("X", 10, ["Y"]) # returns True
scheduler.add_task("Y", 10, ["Z"]) # returns True
scheduler.add_task("Z", 10, ["X"]) # returns False (creates cycle: X -> Y -> Z -> X)
Constraints
- Number of calls to
add_task<= 2,000 - Total calls to
get_next_taskandcomplete_task<= 10,000 task_idconsists of 1 to 30 alphanumeric characters ([a-zA-Z0-9_-])priorityis an integer in the range[-10^9, 10^9]- Each task specifies at most 50 dependencies in
dependencies
Edge Cases
- Dependencies listed in
add_taskthat have already been markedCOMPLETEDshould not block the task. - Calling
complete_taskon a task that isPENDING, alreadyCOMPLETED, or never registered must returnFalse. - Calling
get_next_taskmultiple times when no new tasks have become ready should continue returningNone.
Hints
Hint 1
To detect cycles upon adding a new task, check if the new task is already reachable from any of its declared dependencies in the existing dependency graph.
Hint 2
Maintain an unresolved dependency count for each task along with an adjacency list of downstream dependents to allow O(1) unblocking checks when a task completes.
Hint 3
Use a max-heap (storing (-priority, task_id)) to efficiently retrieve the highest-priority ready task, taking care to handle tie-breaking on task_id.
Follow-up questions
- How would you support a `cancel_task(task_id)` API that cascades cancellations to all downstream tasks unable to ever execute?
- How would you extend this design to run in a concurrent environment where multiple worker threads call `get_next_task` and `complete_task` simultaneously?
- If tasks can time out while in the `IN_PROGRESS` state and need to be re-queued or failed, how would you track and handle those timeouts?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.