Schedule Priority Jobs with Cooldowns
GoogleSoftware Engineer · OnsiteLast updated
Design a recurring job scheduler that selects the highest-priority eligible job and enforces cooldown periods measured by subsequent retrieval invocations.
Problem Statement
Design a recurring job scheduler that supports adding recurring tasks and retrieving the next task to execute based on priority and cooldown constraints.
Implement the JobScheduler class:
JobScheduler(): Initializes the scheduler.void add_job(str job_id, int priority, int cooldown): Registers a recurring job with a unique identifierjob_id, an integerpriority(larger values indicate higher priority), and a non-negative integercooldown. Ifjob_idalready exists, update itspriorityandcooldown.str | None get_next_job(): Selects and returns thejob_idof the highest-priority currently eligible job. If multiple eligible jobs have the same priority, select the one that was added or updated earliest (FIFO tie-breaking). If no jobs are eligible, returnsNone.
Cooldown Semantics
- Each call to
get_next_job()represents one discrete time step (tickt = 1, 2, 3, ...), regardless of whether a job is returned orNoneis returned. - When a job is returned at tick
twith cooldownk, it enters cooldown and cannot be returned again until at leastksubsequent calls toget_next_job()have completed. That is, it becomes eligible again at tickt + k + 1. - A job with
cooldown = 0can be executed again on the very next call (t + 1). - Once scheduled, jobs are recurring: they return to the pool of candidate jobs as soon as their cooldown expires.
Examples
Example 1
scheduler = JobScheduler()
scheduler.add_job("A", priority=10, cooldown=2)
scheduler.add_job("B", priority=5, cooldown=0)
# Call 1 (t=1): "A" (priority 10) is chosen. "A" is on cooldown until t = 1 + 2 + 1 = 4.
scheduler.get_next_job() # Returns "A"
# Call 2 (t=2): "A" is cooling down. "B" is eligible.
scheduler.get_next_job() # Returns "B"
# Call 3 (t=3): "A" is still cooling down. "B" (cooldown 0) is eligible.
scheduler.get_next_job() # Returns "B"
# Call 4 (t=4): "A" cooldown has expired. "A" (priority 10) > "B" (priority 5).
scheduler.get_next_job() # Returns "A"
Example 2
scheduler = JobScheduler()
scheduler.add_job("Task1", priority=5, cooldown=1)
scheduler.get_next_job() # Call 1 (t=1): Returns "Task1"
scheduler.get_next_job() # Call 2 (t=2): Returns None (Task1 eligible at t=3)
scheduler.get_next_job() # Call 3 (t=3): Returns "Task1"
Constraints
1 <= len(job_id) <= 321 <= priority <= 10^90 <= cooldown <= 10^5- At most
2 * 10^5total calls toadd_jobandget_next_job. - Memory usage should be proportional to the number of distinct jobs.
Hints
Hint 1
Separate ready jobs from cooling-down jobs using two distinct data structures: a max-heap for ready jobs and a min-heap (or queue) ordered by ready timestamp for cooling jobs.
Hint 2
Advance a global tick counter on every invocation of get_next_job(), and before picking a job, move all jobs whose cooldown has expired from the cooldown structure into the ready heap.
Hint 3
To handle priority ties by insertion order, include an incrementing insertion counter as a secondary key in the max-heap tuples.
Follow-up questions
- How would you efficiently implement a remove_job(job_id) method without rebuilding the heaps?
- How would you adapt the design for a multithreaded environment where multiple workers call get_next_job() concurrently?
- If cooldown periods were specified in real wall-clock time (e.g., milliseconds) rather than discrete API call ticks, how would the implementation change?
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.