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
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 testsclass HeapEntry { long fireAt; Task task; } // min-heap ordered by fireAtschedule() and cancel()
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
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
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.