Free preview

Why this matters: the dispatcher loop is where this design is won or lost, and it is short enough that every line carries weight. Interviewers watch for three things here: the wait that can't miss an earlier insert, the predicate re-check on wakeup, and the dispatcher handing work away instead of running it.

The records

java
enum State { PENDING, READY, RUNNING, SUCCEEDED, FAILED, SKIPPED, CANCELLED } class Task { String id; Runnable work; Schedule schedule; // at / after / every(interval) / afterTasks State state = State.PENDING; int unfinishedParents; // dependency indegree; 0 = time rules apply List<Task> children; // reverse edges for propagation } interface Clock { long now(); } // injected; fake in tests
java
class HeapEntry { long fireAt; Task task; } // min-heap ordered by fireAt

schedule() and cancel()

java
void schedule(Task t) { synchronized (lock) { validateNoCycles(t); // reject at submission if (t.unfinishedParents > 0) { waiting.add(t); // dependency-wait set, not the heap } else { heap.push(new HeapEntry(t.schedule.firstFireAt(clock.now()), t)); lock.notify(); // an earlier deadline may now exist } } } void cancel(String taskId) { synchronized (lock) { Task t = byId.get(taskId); if (t != null && !t.state.isTerminal()) t.state = State.CANCELLED; // heap entry stays; the dispatcher discards cancelled tasks on pop — // lazy removal beats surgically deleting from heap middles. } }

Two small decisions worth narrating: the notify() on every schedule — any insert might beat the current earliest deadline, and waking the dispatcher to re-check is cheap; and lazy cancellation — marking the task and letting the pop discard it costs one wasted heap entry instead of an O(n) heap deletion.

The dispatcher loop

java
void dispatcherLoop() { while (running) { Task due = null; synchronized (lock) { while (true) { long now = clock.now(); if (heap.isEmpty()) { lock.wait(); continue; } long topFire = heap.peek().fireAt; if (topFire <= now) { due = heap.pop().task; break; } lock.wait(topFire - now); // deadline wait; insert notifies // loop re-checks: maybe woken by an earlier insert, // maybe spuriously — the predicate decides, not the wakeup } } if (due.state == State.CANCELLED) continue; // lazy removal dispatch(due); } }

The while (true) around the wait is the discipline the thread-pool chapter drilled: never assume why you woke. An earlier insert, a spurious wakeup, and an actually-due deadline all land in the same place — a re-check of the heap top against the clock — and all three are handled by the same four lines.

dispatch(): hand off, then handle what comes back

java
void dispatch(Task t) { t.state = State.RUNNING; pool.submit(() -> { // the GIVEN pool runs it boolean ok = runSafely(t.work); // exceptions caught, never thrown onComplete(t, ok); }); } void onComplete(Task t, boolean ok) { synchronized (lock) { t.state = ok ? State.SUCCEEDED : State.FAILED; if (ok) { for (Task child : t.children) if (--child.unfinishedParents == 0) promote(child); } else { markDescendantsSkipped(t); // visible, terminal — never silent } if (t.schedule.isRecurring() && ok != /*cancelled*/ false) { long next = t.schedule.lastFireAt + t.schedule.interval; // aligned, no drift t.state = State.PENDING; heap.push(new HeapEntry(next, t)); lock.notify(); } } }

Three narrations to make while writing this: completion — not dispatch — is where recurrence reinserts, which is exactly what makes the skip-overlap rule from lesson 01 fall out for free (a running task simply isn't in the heap to fire again); the next fire is computed from the scheduled time, the aligned no-drift choice from lesson 02, said out loud; and dependency promotion is three lines because the indegree counters did the design work already.

What you'd say about complexity

Schedule and reinsert are O(log n) heap pushes; the dispatcher's pop is O(log n); cancellation is O(1) marked, amortized by lazy discard; dependency propagation on a completion is O(children). And the claim that matters most is about wakeups, not big-O: the dispatcher wakes exactly once per due fire plus once per potentially-earlier insert — no polling tick, no idle churn. With a fake clock, every one of these claims is a unit test, which is the sentence to end the implementation act on.

Key takeaway

The implementation is one guarded loop and two handoffs: schedule pushes and notifies, the dispatcher deadline-waits and re-checks its predicate before trusting any wakeup, and dispatch submits to the given pool with completion driving both recurrence reinsertion (aligned, drift chosen on purpose) and indegree promotion. Lazy cancellation and skip-by-absence fall out of the same shape — the sign the structure was right.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue