Coordination, Waiting & Cancellation

Swallowed Interrupt During Shutdown

Swallowed Interrupt During Shutdown: practice a Java concurrency bug with symptoms like Shutdown hangs, Worker ignores cancellation, Process still alive....

  • Cancellation
  • Cancellation
  • Interrupts
  • Java
  • Beginner

Production symptoms

  • Shutdown hangs
  • Worker ignores cancellation
  • Process still alive

Failure scenario

Code

Java example
class Poller implements Runnable {
    private final BlockingQueue<Job> jobs;

    public void run() {
        while (true) {
            try {
                Job job = jobs.take();
                process(job);
            } catch (InterruptedException ignored) {
                // Cancellation signal is lost; the loop waits again.
            }
        }
    }
}

Prod Symptoms

A deployment or service shutdown interrupts a background worker, but the worker catches the signal and returns to its polling loop.

Key signal: Interrupt requests cooperative cancellation; it does not forcibly stop a thread. Swallowing the signal lets the task continue.

  • Shutdown is requested, but the executor does not terminate
  • awaitTermination returns false or the process exceeds its shutdown budget
  • The worker logs an interrupt and then appears again in sleep(), wait(), or BlockingQueue.take()
  • CPU stays low because the worker is blocked again, not spinning
  • The issue appears during shutdown or redeploy, while normal processing looks healthy

Run Locally

  • The worker enters an interruptible sleep
  • shutdownNow interrupts the worker
  • Thread.sleep throws InterruptedException and clears the interrupt status
  • The catch block ignores the cancellation request
  • The worker starts another loop iteration and sleeps again
  • awaitTermination returns false
  • The JVM remains alive until you stop the process

What to look for

  • A pool thread still alive after shutdown
  • A catch block for InterruptedException that only logs, ignores, or continues
  • Thread dumps showing the worker back in sleep, wait, or BlockingQueue.take after cancellation
Run
javac SwallowedInterruptShutdownDemo.java
java SwallowedInterruptShutdownDemo
Inspect while stuck
jps
jstack <pid>
jcmd <pid> Thread.print
SwallowedInterruptShutdownDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class SwallowedInterruptShutdownDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService worker = Executors.newSingleThreadExecutor(named("poller"));

        worker.submit(() -> {
            while (true) {
                try {
                    System.out.println("poller waiting for work");
                    Thread.sleep(10_000);
                } catch (InterruptedException ignored) {
                    System.out.println("poller swallowed interrupt and keeps running");
                }
            }
        });

        Thread.sleep(500);
        System.out.println("shutdownNow sends interrupt");
        worker.shutdownNow();

        boolean terminated = worker.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("terminated = " + terminated);

        if (!terminated) {
            System.out.println("Bug reproduced. Use Ctrl+C to stop the process.");
        }
    }

    private static ThreadFactory named(String name) {
        return runnable -> new Thread(runnable, name);
    }
}

Note: shutdownNow interrupts the sleeping worker. Thread.sleep throws InterruptedException and clears the interrupt status, but the catch block continues the loop and sleeps again.

Diagnosis and fix

Explanation

Interrupt is a cooperative signal whose meaning is defined by the task's cancellation contract.

Key signal: Catching InterruptedException is a cancellation decision, not ordinary error handling.

  • shutdownNow attempts to interrupt actively executing executor tasks
  • sleep(), wait(), join(), and interruptible BlockingQueue operations can report interruption with InterruptedException
  • Throwing InterruptedException clears the thread's interrupted status
  • If the catch block continues without exiting, propagating, or restoring the signal, cancellation is lost
  • Restoring the status preserves the signal, but the task must still stop when interruption means cancellation
  • Not every blocking API is interruptible, so some resources require their own cancellation mechanism

How to Diagnose

Start with the shutdown timeline, then trace the cancellation signal into the worker.

  • Confirm that lifecycle code called shutdownNow(), Future.cancel(true), or Thread.interrupt()
  • Check whether awaitTermination exceeded the shutdown budget
  • Correlate the interrupt or shutdown log with a worker that later returns to sleep(), wait(), or queue polling
  • Inspect catch blocks that log or ignore InterruptedException and continue looping
  • Remember that InterruptedException clears the status, so a later thread dump may show an ordinary waiting thread rather than evidence of the earlier interrupt
  • Verify whether the blocking operation actually supports interruption
If still running
jps
jstack <pid>
jcmd <pid> Thread.print
Typical output
shutdownNow sends interrupt
poller swallowed interrupt and keeps running
terminated = false
Bug reproduced. Use Ctrl+C to stop the process.

Note: A thread dump shows that the worker is still alive, but logs and code review are needed to prove that an earlier interrupt was swallowed.

How to Fix

  • Exit the task when interruption means cancellation
  • Propagate InterruptedException when the caller can decide how to handle cancellation
  • When propagation is impossible, restore the status with Thread.currentThread().interrupt()
  • Do not restore the status and then continue an unconditional polling loop
  • Treat continuing after interruption as an explicit, documented policy
  • Keep awaitTermination bounded so failed shutdown remains observable
  • Use the cancellation mechanism appropriate for blocking operations that do not respond to interrupt
InterruptAwareShutdownFixed.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class InterruptAwareShutdownFixed {
    public static void main(String[] args) throws Exception {
        ExecutorService worker = Executors.newSingleThreadExecutor(named("poller"));

        worker.submit(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("poller waiting for work");
                    Thread.sleep(10_000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("poller observed interrupt and stops");
                return;
            }
        });

        Thread.sleep(500);
        System.out.println("shutdownNow sends interrupt");
        worker.shutdownNow();

        boolean terminated = worker.awaitTermination(2, TimeUnit.SECONDS);
        System.out.println("terminated = " + terminated);
    }

    private static ThreadFactory named(String name) {
        return runnable -> new Thread(runnable, name);
    }
}

Note: The fixed worker treats interruption as a shutdown signal, restores the interrupted status, and lets the task finish.