Implement A Prioritized Task Scheduler With Parent Dependencies
RipplingSoftware Engineer · Technical ScreenLast updated
Implement a task scheduler that manages parent-child task dependencies, applies custom multi-field tie-breaking priority rules, supports filtering, and handles dependency cycles.
Problem Statement
In a distributed workflow platform, background jobs often depend on the completion of prerequisite parent tasks. You are asked to implement an in-memory TaskScheduler that manages task dependencies, selects the next executable task according to a strict multi-attribute priority order, supports filtering, and gracefully detects circular dependencies.
A task cannot be executed until all of its parent dependencies have been completed. Once a task's dependencies are satisfied, it becomes ready for execution.
Implement the TaskScheduler class with the following methods:
add_task(task_id: str, parent_ids: list[str], due_date: int, is_high_priority: bool, created_at: int) -> None: Registers a task with its metadata and list of parent prerequisite task IDs.complete_task(task_id: str) -> None: Marks a task as finished. This may unblock dependent child tasks, making them ready.get_next_task(high_priority_only: bool = False) -> str | None: Retrieves and claims (removes from ready pool) the next highest-priority task ready for execution. Ifhigh_priority_onlyisTrue, only tasks withis_high_priority == Truemay be returned. If no matching ready task exists, returnsNone.has_deadlock() -> bool: ReturnsTrueif there are remaining incomplete tasks in the scheduler, but none of them can be executed (e.g., due to a dependency cycle or an uncompleted prerequisite that was never added/completed).
Priority Ordering Rules
When multiple tasks are ready, they must be selected using the following tie-breaking hierarchy:
- Due Date: Lower
due_datevalue (earliest deadline first). - Priority:
is_high_priority == Truebeforeis_high_priority == False. - Creation Time: Lower
created_atvalue (earliest created first). - Task ID: Lexicographically smaller
task_id(e.g.,'task_a' < 'task_b').
Example
scheduler = TaskScheduler()
# Add tasks
# Task A has no dependencies
scheduler.add_task("A", [], due_date=100, is_high_priority=False, created_at=10)
# Task B depends on A
scheduler.add_task("B", ["A"], due_date=50, is_high_priority=True, created_at=20)
# Task C has no dependencies
scheduler.add_task("C", [], due_date=100, is_high_priority=True, created_at=15)
# Ready tasks initially: A and C (B is waiting on A)
# Comparing A vs C:
# Due dates are equal (100 vs 100).
# Priority: C is high priority (True), A is not (False). C wins.
assert scheduler.get_next_task() == "C"
# Next ready task is A
assert scheduler.get_next_task() == "A"
# No ready tasks right now; B is still waiting on A to complete
assert scheduler.get_next_task() == None
assert scheduler.has_deadlock() == False # A is in-progress / uncompleted
# Complete A, which unblocks B
scheduler.complete_task("A")
assert scheduler.get_next_task() == "B"
# All tasks claimed and completed
scheduler.complete_task("B")
assert scheduler.has_deadlock() == False
Constraints
1 <= task_id.length <= 64(strings containing alphanumeric characters and underscores).0 <= parent_ids.length <= 100.0 <= due_date, created_at <= 10^9.add_taskwill be called at most10^4times with uniquetask_ids.complete_taskwill only be called with validtask_ids that have been claimed viaget_next_taskor were previously added.
Hints
Hint 1
Model the priority comparator as a 4-tuple: `(due_date, 0 if is_high_priority else 1, created_at, task_id)` to let standard min-heaps sort ready tasks correctly.
Hint 2
Track the count of unsatisfied parent dependencies for each task (similar to Kahn's algorithm for topological sorting) so that completing a task decrements child in-degrees in `O(1)` per dependent edge.
Hint 3
For `high_priority_only = True` filtering, maintain separate ready queues for high-priority and normal tasks, or pop non-matching elements and restore them if necessary.
Follow-up questions
- How would you support task timeouts where a claimed task that is not completed within `T` seconds is returned to the ready pool?
- If millions of tasks are added dynamically across multiple worker threads, how would you redesign the concurrency and locking model?
- How would you handle dynamic task cancellations and propagate cancellations downstream through child dependencies?
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.