Job Scheduler
Overview
Implement a scheduler that runs functions later instead of right now. A caller can ask to run a job after a delay, at a specific time, or on a repeating interval. The scheduler keeps a worker running in the background that fires each job as close to its due time as possible.
The core data structure is a min-heap (priority queue) keyed by each job's next run time, so the soonest job is always on top. The worker sleeps until that job is due rather than spinning, and wakes early if a sooner job is added. Getting the waiting right — sleep exactly long enough, but stay responsive to new jobs — is the heart of the problem.
Interfaces
class JobScheduler:
def __init__(self):
self._heap = [] # (run_at, job_id, fn, interval)
self._lock = Lock()
self._cond = Condition(self._lock)
# start one background worker thread running _run_loop
def schedule(self, fn, delay):
"""Run fn once after `delay` seconds. Return a job id."""
...
def schedule_at(self, fn, timestamp):
"""Run fn once at an absolute time."""
...
def schedule_every(self, fn, interval):
"""Run fn repeatedly, every `interval` seconds."""
...
def cancel(self, job_id):
"""Stop a scheduled job from running (again)."""
...
def _run_loop(self):
# pop due jobs and run them; otherwise wait until the next is due
...
The worker peeks the earliest job. If it is due, it runs it (and, for a recurring job, re-inserts it with the next run time). If not, it waits on a condition variable until the due time — or until schedule signals that a sooner job arrived.
Inputs and outputs
- Input: a function plus a delay, an absolute time, or a repeat interval.
- Output: each job runs once at (or just after) its due time; recurring jobs keep firing until cancelled.
Requirements
- Jobs fire in time order, regardless of the order they were added.
- Adding a job that is due sooner than everything queued must wake the worker early — no fixed polling interval.
- The worker waits, it does not busy-spin.
cancelreliably prevents future runs of a job.- All access to the shared queue is synchronized.
Examples
s = JobScheduler()
s.schedule(lambda: print("once"), delay=5) # fires at t+5s
s.schedule_every(heartbeat, interval=1) # fires every 1s
jid = s.schedule(cleanup, delay=60)
s.cancel(jid) # cleanup never runs
If you add a delay=1 job while a delay=10 job is already waiting, the new job still fires first.
Think about it
The worker is asleep until a job 10 seconds away, and a 1-second job arrives.
With a plain fixed sleep, when does that new job actually fire?
How does the worker get told to wake up and recompute how long it should sleep?
Follow-ups
One slow job shouldn't delay the next. How would you run jobs on a thread pool instead of inline in the worker?
How would you support cron-style schedules (specific minutes and hours) on top of fixed intervals?
If the process restarts, how would you make the schedule survive so jobs aren't lost?
And more...
FANG Crash Course
Fullset Newest FANG Coding/System Design Questions, Optimal Solutions and Explanations.
Step_1 Matched with a FAANG+ Senior Engineer
Step_2 Fullset FANG Coding/System Design Questions
Step_3 Solutions + step by step Explanation
