LLD-017 · DESIGN PRACTICE
Task Scheduler / Thread Pool
Own queue admission, worker lifetime and cooperative cancellation with precise state transitions.
Local Java 21 reference verified: 26 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design a fixed-worker scheduler for immediate and delayed tasks. Bound pending work, run user code outside scheduler ownership, expose results and distinguish graceful shutdown, immediate shutdown, cancellation request and actual local task completion.
Before you start
- Producer/consumer queues and heaps
- Thread interruption and cooperative cancellation
- Futures and task state machines
- Monotonic clocks and bounded admission
Scope and explicit decisions
- Local Java worker pool, 1..32 daemon workers and a positive pending-queue capacity. No persistence, distributed leases, automatic retries, recurring schedules or untrusted-code sandbox is implemented. Tasks are trusted Callable<String> bodies.
- The priority queue orders by due elapsed nanoseconds then submission ID. Delays are nonnegative and deadlines use checked arithmetic. A trusted injected clock must be nonnegative, nondecreasing, fast and nonthrowing.
- Workers use timed monitor waits and wake when new/earlier work arrives. Tests advance an AtomicLong clock then call wake, so correctness checks do not require arbitrary sleeps. Real termination waits use System.nanoTime, independent of the injected task clock.
- Queue capacity limits QUEUED tasks, not running workers or retained history. Total executing task count is bounded by worker count; results/records remain retained for process lifetime. There is no user fairness or priority-class policy.
- Cancel QUEUED removes it and completes its result as cancelled. Cancel RUNNING sets CANCEL_REQUESTED and interrupts the worker; a task that ignores/catches interruption may keep running. It becomes CANCELLED only when its local body returns/throws.
- The scheduler cannot retract remote effects performed by task code. A cancelled future does not prove an email/charge was undone. User callbacks and future awaits run outside the queue monitor.
- Graceful shutdown rejects new submissions and continues queued tasks, including delayed ones when due. Immediate shutdown cancels pending tasks and requests interruption of running tasks. Neither forcibly kills uncooperative user code.
- awaitTermination has a bounded real-time wait (0..60,000ms) and returns false if workers remain alive. Daemon threads do not keep a process alive; they do not make tasks durable. Fatal VM errors may terminate a worker without replacement.
- A completed task’s interruption flag is cleared before the next task runs. Ordinary task failure is stored in its future. No public raw CompletableFuture is exposed for arbitrary callbacks under scheduler locks.
Worked examples
Input: One worker, queue capacity2, future tasks at1s; cancel one then insert100ms task.
Outcome: Third submission initially rejects; cancelled slot permits earlier task, which runs first when fake time advances.
Admission capacity and deadline order are distinct controls.
Input: Running task catches interruption and waits for explicit release.
Outcome: cancel reports CANCEL_REQUESTED; after body returns the future becomes CANCELLED.
Interruption is a cooperative request, not proof of immediate termination.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Try before revealing
Hint 1
Which resources are queued and which are already executing?
Hint 2
What wakes a worker when an earlier deadline arrives?
Hint 3
How can callers tell cancellation requested from locally stopped?
Requirements and invariants
R1 — Pending queue count never exceeds configured admission capacity. submit protects I1.
R2 — Only an owned worker can transition a queued task to running and execute it outside the queue lock. loop protects I2.
R3 — Cancellation requested is not reported as stopped until local execution ends. cancel protects I3.
R4 — Shutdown stops new admission while preserving the declared queued/running policy. shutdown protects I1.
I1: queue.size<=queueCapacity; admission closes once shutdown starts.
I2: At most workerCount task bodies execute, and a task has one current worker owner.
I3: RUNNING→CANCEL_REQUESTED may precede actual stop; only completion produces terminal CANCELLED for running work.
Derive the design
An executor needs admission and lifecycle beyond a worker loop
A single worker polling a FIFO can run tasks, but it cannot order future deadlines or protect the process against unlimited pending work. The reference adds a deadline heap, bounded admission and owned worker state. It does not expand into a durable scheduler merely because the class is named Scheduler.
Alternative: Java executor services already supply robust production building blocks. This teaching implementation exposes the mechanics; a production choice should consider mature libraries, real clock behavior, thread factories, metrics and shutdown guarantees rather than reimplement them without need.
Name queue and running capacity separately
A worker limit controls concurrency; queue capacity controls accepted backlog. Neither alone caps retained history or total bytes.
Use condition loops around waits
Spurious wakeups, new earlier deadlines and shutdown all require rechecking the actual state under the lock. Never assume one notify means a task is ready.
Execute outside ownership
Claim a queued task and owner thread under the lock, then call arbitrary user code outside it. Result waiting must also be outside the lock so workers can finish.
Make cancellation visible until completion
Preserve CANCEL_REQUESTED while code still runs. This gives callers an honest state and prevents unsupported claims about remote effects.
Responsibilities and ownership
Scheduler
Owns: Queue, records, workers, clock checks and lifecycle flags
Methods: submit, cancel, shutdown, awaitTermination
Trusted local executor, not a sandbox or durable job platform.
Task
Owns: Due time, body, result, owner thread and state
Methods: Private lifecycle state
Worker/result ownership is coordinated under one lock.
PriorityQueue
Owns: Due-order admission plan
Methods: Peek/remove/insert
Heap order is not global fairness or preemption.
View / await
Owns: Immutable status and bounded wait interface
Methods: view, await
A terminal result describes local execution, not external side-effect rollback.
Local trusted callables only; no durable execution or sandbox is implied.
- Submission + due time
↓ Reject closed/full admission
Bounded deadline heap↓ Wait/recheck earliest deadline
Worker claims RUNNING↓ One owner per task
Body outside lock → terminal result
A terminal local cancellation does not undo remote side effects.
- QUEUED or RUNNING
↓ Pending and active cases differ
Queued: remove / Running: interrupt request↓ Never force-stop user code
CANCEL_REQUESTED while body may run↓ Clear reused worker interruption
CANCELLED after local completion
Data model, schema and relationships
A scheduler owns tasks, due ordering and worker claims. A result is not the same thing as a cancellation request: a running body may ignore interruption until it exits.
Implementation boundary: Local daemon workers execute trusted callables and private futures. Proposed task/attempt persistence does not serialize live Callable closures or make this pool a durable scheduler.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
scheduler
Admission and worker policy.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Pool identity |
worker_count | integer · VALUE | Fixed concurrency |
queue_capacity | integer · VALUE | Pending bound |
Code mapping: Scheduler.
- Worker count 1..32; queue positive.
scheduled_task
Accepted logical task and due time.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | bigint · PK | Submission identity |
scheduler_id | uuid · FK | Pool owner · scheduler.id |
due_tick | bigint · VALUE | Elapsed deadline |
state | enum · VALUE | QUEUED RUNNING CANCEL_REQUESTED etc |
Code mapping: Task and priority queue.
- Queued count<=capacity; retained result count is not queue depth.
worker
Local worker identity.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Thread identity |
scheduler_id | uuid · FK | Pool owner · scheduler.id |
state | enum · VALUE | Waiting running stopped |
Code mapping: Owned worker thread.
- Live thread/future references are process-local, never persistence handles.
task_attempt
Execution ownership and outcome.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Attempt identity |
task_id | bigint · FK | Accepted task · scheduled_task.id |
worker_id | text · FK | Claiming worker · worker.id |
outcome | enum · VALUE | Running success failure cancelled |
Code mapping: Task.worker and result lifecycle.
- Reference executes each accepted task once; retry/lease history would be a durable extension.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| scheduler → scheduled_task accepts tasks | 1 parent per child; 0..* children per parent | scheduled_task.scheduler_id → scheduler.idPool admission bounds queued work independently of retained results. |
| scheduler → worker owns workers | 1 parent per child; 0..* children per parent | worker.scheduler_id → scheduler.idFixed concurrency limits active task bodies. |
| scheduled_task → task_attempt executes | 1 parent per child; 0..* children per parent | task_attempt.task_id → scheduled_task.idReference permits one attempt; durable retries require a new contract. |
| worker → task_attempt claims work | 1 parent per child; 0..* children per parent | task_attempt.worker_id → worker.idA worker executes many historical tasks, one body at a time. |
Transaction boundaries
- Claim due head under queue monitor, mark RUNNING and assign worker; execute body after releasing monitor.
- Queued cancel removes and completes cancelled. Running cancel requests interrupt, then transitions terminal only when body exits.
Indexes and uniqueness
- Heap(due_tick,id) orders due work deterministically; linear removal is a known queued-cancel cost.
- Durable variant needs absolute due instant, versioned handler identifier/input and fenced claim—not a saved nanosecond clock value.
Schema traps and failure cases
- Persisting System.nanoTime-based deadlines across restart misinterprets the clock origin.
- Immediate shutdown cannot forcibly terminate a task ignoring interruption.
Trace the records through one operation
- One delayed task occupies a pending slot until due.
- Worker claims it; cancellation changes state to CANCEL_REQUESTED.
- Body exits; result becomes CANCELLED and worker interrupt is cleared before reuse.
Method contracts
submit(Callable<String>,delayNanos) → taskId
Reject closed/full queue or invalid delay. Compute deadline, add one queued task and notify waiting workers.
cancel(taskId) → boolean
Queued cancellation is immediate. Running cancellation records request and interrupts; completed tasks return false.
view(taskId); await(taskId,Duration)
Read immutable local state or wait outside scheduler ownership. Future may return, fail, time out or cancel; waiting does not hold the queue monitor.
shutdown(immediately)
Always stop admission. Graceful leaves accepted work; immediate cancels queued work and requests interruption from running owners.
wake(); awaitTermination(millis)
wake supports changed injected clock observations. Termination wait uses real elapsed time and reports uncooperative workers honestly.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Bounded producer/consumer admission
- what
- Accept finite pending work and separate it from bounded execution slots.
- when
- Producers can submit faster than tasks finish.
- why
- An unlimited queue hides overload until memory or latency fails.
- how
- submit checks queueCapacity under the same owner used by workers, inserts the task and notifies. workerCount fixes executing bodies; running tasks do not consume queue slots.
Code: submit, queueCapacity, workers · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: Reject, caller-runs, block-with-deadline or tenant quotas are different overload policies and must be chosen deliberately.
Cost: Retained completed tasks/results are not bounded by queue capacity.
Counterexample: Claim a queue of100 means only100 task records can ever exist.
Test: Fill a future-due queue and assert extra submission rejects; prove at most two task bodies run with two workers.
Delay heap and wakeup protocol
- what
- Choose the earliest due task without scanning all future tasks.
- when
- Immediate and delayed work coexist.
- why
- Sleeping until an old deadline without notification can ignore a newly submitted earlier task.
- how
- PriorityQueue sorts due/ID; workers wait for head deadline and submit/wake/shutdown notify. Recheck the condition in a loop after every wake or interruption.
Code: queue, loop, wake · Invariants: I2
Alternatives, cost and failure example
Alternative: A DelayQueue or scheduled executor is an alternative; it still needs cancellation/admission/lifecycle contracts.
Cost: Insertion/removal is O(log Q), but cancelling a particular Task uses PriorityQueue.remove(Object), O(Q).
Counterexample: A worker sleeps for the old one-second task after a new 100ms task arrives.
Test: Advance fake time to the earlier due task, notify and verify it finishes while later task stays QUEUED.
Cooperative cancellation state
- what
- A request to stop is separate from evidence that user execution stopped.
- when
- A running task may ignore interruption or already perform external work.
- why
- Reporting CANCELLED immediately can falsely imply the operation ended or was undone.
- how
- cancel marks CANCEL_REQUESTED and interrupts the owner. Worker records CANCELLED only after the body returns/throws, then clears interruption before the next task.
Code: cancel, loop · Invariants: I3
Alternatives, cost and failure example
Alternative: A process boundary can enforce stronger local resource termination for untrusted work, but cannot undo external side effects already committed.
Cost: Uncooperative tasks retain worker capacity; no force-stop is attempted.
Counterexample: Task catches InterruptedException and keeps running while UI already shows completed cancellation.
Test: Use latches to hold a task after it catches interrupt; inspect CANCEL_REQUESTED before release and CANCELLED afterwards.
Owned shutdown and result boundaries
- what
- Stop admission, decide queued policy and wait for actual worker exit.
- when
- Application lifecycle or overload requires cleanup.
- why
- A close flag alone neither drains future tasks nor ends running code.
- how
- shutdown(false) drains due accepted work; shutdown(true) cancels queue and interrupts current owners. awaitTermination uses a real elapsed budget and returns a boolean.
Code: shutdown, awaitTermination, await · Invariants: I1, I3
Alternatives, cost and failure example
Alternative: A higher-level service may use grace deadline then immediate cancellation, retaining unresolved work for durable recovery.
Cost: Graceful delayed tasks may keep workers alive until future due time; injected clocks must advance.
Counterexample: Call shutdown and assume all effects are finished despite a task blocked in noninterruptible I/O.
Test: Check rejection after shutdown, queued cancellation on immediate shutdown and true termination only after cooperative bodies finish.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
submit validates before admission
Deadline overflow or full/closed state rejects before inserting a new task. Notify lets workers reconsider the heap head.
loop separates claim from execution
The synchronized loop waits/rechecks and assigns RUNNING ownership. Body invocation occurs outside the monitor, then one terminal result is recorded under ownership.
cancel differentiates pending and active
Queued tasks can be removed immediately. Running tasks retain their owner until completion, so the state records a cancellation request rather than a fictional stop.
awaitTermination uses a separate real-time clock
A frozen fake task clock must not make a test’s termination timeout freeze too. Elapsed real time bounds joins.
Complexity and limits
- submit/poll are O(log Q) queue operations for Q pending tasks, with expected O(1) task-ID lookup. cancel of a queued Task is O(Q) due to heap identity removal.
- Shutdown-now scans retained H task records plus Q pending tasks: O(H+Q). Views are expected O(1); termination wait is bounded by supplied real-time budget, excluding caller scheduling.
- Space includes Q pending bodies, up to W running bodies and unbounded retained records/results H. Task payload bytes are not capped by count limits.
- Due scheduling and actual task completion latency depend on trusted clock, OS scheduling and user code. No real-time, fairness or distributed execution bound is claimed.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Delay ordering and bounded admission
Step 1 of 2: Queue future tasks at t=0
Capacity reached; extra submission rejects
Invariant: Pending count bounded
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Queue future tasks at t=0 | Capacity reached; extra submission rejects | Pending count bounded |
| Insert earlier due task after cancellation frees slot | Worker wakes and runs earlier deadline first | Heap order is due time then task ID |
Cancellation requested is not stopped
Step 1 of 2: Cancel running task that catches interrupt
CANCEL_REQUESTED; user body still running
Invariant: No unsafe force-stop promise
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Cancel running task that catches interrupt | CANCEL_REQUESTED; user body still running | No unsafe force-stop promise |
| Task body returns after explicit release | CANCELLED; future cancelled | Terminal state follows actual local completion |
Mistakes and counterexamples
Hold the queue monitor during task code
Failing case: A slow Callable blocks cancellation and all producers.
Repair: Claim under lock and execute outside it.
Treat interrupt as force-stop
Failing case: Task catches it and continues.
Repair: Keep CANCEL_REQUESTED until actual body completion.
Forget to wake for an earlier deadline
Failing case: New immediate work waits behind an old future task.
Repair: Notify and recheck heap head in a condition loop.
Advertise bounded memory from queue count
Failing case: Completed results remain retained forever.
Repair: Account for body bytes, workers and result-history retention separately.
Engineer → Senior → Staff
Implement bounded immediate/delayed execution.
Executed: deadline ordering, queue rejection, results and ordinary task failures.
Change exercise
Test equal due times and explain which ordering the heap guarantees.
Worked answer guidance
The heap ties by task ID, so claim order is deterministic. With multiple workers, completion order can differ because task bodies run concurrently.
Interview follow-ups
Why recheck after wait?
Wakeups can be spurious or caused by a different event; the queue/shutdown/clock conditions are authoritative.
Does await hold the scheduler lock?
No. Waiting under that lock could block the worker that must complete the result.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.