🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
Exception in thread "main" java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.devinhyderabad.worker.JobRunner.run(JobRunner.java:5)
at com.devinhyderabad.worker.WorkerMain.main(WorkerMain.java:6)
// Interrupted inside Object.wait()/join(): NO message — the bare form:
Exception in thread "main" java.lang.InterruptedException
at java.base/java.lang.Object.wait(Native Method)
at java.base/java.lang.Object.wait(Object.java:338)
at com.devinhyderabad.worker.QueueDrainer.drain(QueueDrainer.java:7)⚡ Quick Fix Works 80% of the time
Either propagate InterruptedException upward or restore the flag before continuing — never swallow it.
try {
Job job = queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore: cancellation stays visible
return; // exit the loop promptly
}🧠 Why this Happens
Tap to expand the deep technical explanation
Every thread carries one boolean interrupt STATUS FLAG — not a queue of signals. interrupt() sets it; blocking primitives poll it on entry and after wakeups, and crucially they CLEAR the flag while throwing InterruptedException. That clearing is the entire tragedy: a swallowed exception leaves no trace of the request anywhere, so outer layers can never observe that cancellation was asked for. The message asymmetry is incidental hotspot plumbing — the sleep path constructs its exception with "sleep interrupted", the wait/join paths pass none. Checked-ness is deliberate design friction: the compiler forces every handler to consciously choose between propagating, restoring, or genuinely consuming the stop request.
The HITEC City Parking Spot Analogy:
An interrupt is a tap on the shoulder asking a swimmer to leave the pool. Swallowing it erases the tap from history — the lifeguard believes everyone left willingly. Re-raising the flag is signing the visitor book on the way out so the next checkpoint knows the evacuation is still underway.
🔁 How to Reproduce Confirm this is your error
Set Thread.currentThread().interrupt() BEFORE calling Thread.sleep(60_000) or lock.wait() — both throw instantly (zero waiting): sleep carries the message "sleep interrupted"; wait throws bare. Captured verbatim on OpenJDK 17; messages identical on Temurin 25 though native frames rename there (Thread.sleepNanos0, Object.wait0).
🛠️ Solutions (5 Ways to Fix)
Restore the interrupt flag, then stop the work
👉 Use this whenever you must catch InterruptedException inside a loop or task body.
Blocking methods CLEAR the flag while throwing, so catching without re-interrupting ERASES the cancellation request — thread pools then leak stuck workers and shutdown hangs forever. The pattern is always: catch, Thread.currentThread().interrupt(), exit promptly.
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
process(queue.take());
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // re-mark
break; // leave cleanly
}
}
}Propagate instead of handling when the caller owns lifecycle
👉 Use this for library/service methods whose callers sit above scheduling logic.
Declaring throws InterruptedException keeps the cancellation decision where lifecycle policy lives (executors, frameworks). Converting to unchecked exceptions severs that contract and forces every layer to reinvent shutdown handling.
public Report generate(Input in) throws InterruptedException {
// caller decides: retry, reschedule, or abandon
var part1 = heavyStepOne(in);
var part2 = heavyStepTwo(part1);
return assemble(part2);
}Decode the two pasted shapes at a glance
👉 Use this to triage which blocking call was interrupted before reading any code.
Message-bearing "sleep interrupted" comes specifically from Thread.sleep; the bare message-less form comes from Object.wait(), join(), and most j.u.c conditions. Semantics are identical — the flag was set during blocking — only the throw site differs, and native frame names keep evolving across builds.
// Shape A (Thread.sleep):
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
// Shape B (wait/join/j.u.c.):
java.lang.InterruptedException
at java.base/java.lang.Object.wait(Native Method)Cooperate with Future.cancel(true) in pooled tasks
👉 Use this when tasks run inside executor services and users hit cancel or shutdown-now.
cancel(true) sets the worker's flag; tasks honor it either by getting InterruptedException from blocking calls or by polling Thread.currentThread().isInterrupted() between CPU-bound chunks. Ignoring either channel makes shutdown-now indistinguishable from a hang.
Future<Report> f = pool.submit(this::expensiveReport);
...
f.cancel(true); // sets flag -> blocking calls throw, loops can poll
// Inside expensiveReport:
if (Thread.currentThread().isInterrupted()) throw new CancellationException();Swallow-spotting in reviews and thread dumps
👉 Use this to hunt the anti-pattern across an existing codebase or a hung service.
Empty catch blocks around InterruptedException silently kill cancellation; grep finds them instantly. On a running JVM, jstack revealing threads parked in TIMED_WAITING despite a pending shutdown request is the runtime symptom of exactly that swallow.
# Static sweep:
grep -rn --include=*.java -A2 "catch (InterruptedException" src/ | less
# Runtime confirmation:
jstack <pid> | grep -B2 -A4 "TIMED_WAITING"📋 Version Notes
"Sleep interrupted" wording already present for Thread.sleep; bare form from wait/join; default stacks platform-dependent.
Semantics unchanged; internal native method names begin diverging between builds.
Captured verbatim here: Thread.sleep(Native Method) and Object.wait(Object.java:338) frame shapes.
Virtual threads honor interruption identically (same restore rule!) — native frames renamed again on newest builds (sleepNanos0, wait0 observed on 25).
🛡️ How to Prevent This Next Time
Standardize a project snippet (catch -> restore -> exit), ban empty catch blocks via Checkstyle/ErrorProne rules, document every blocking call in public APIs, and smoke-test shutdown paths so cancellations complete within your SLA.