apache/dubbo · error · IllegalStateException
cannot be started once stopped
Error message
cannot be started once stopped
What it means
IllegalStateException thrown by HashedWheelTimer.start() when the worker state is already WORKER_STATE_SHUTDOWN. Once a HashedWheelTimer is stopped (via stop() or GC/finalize), it cannot be restarted — the worker thread has terminated and internal state is torn down. start() is normally called lazily by newTimeout() or explicitly by the user; calling it on a stopped timer is a programming error.
Source
Thrown at dubbo-common/src/main/java/org/apache/dubbo/common/timer/HashedWheelTimer.java:325
/**
* Starts the background thread explicitly. The background thread will
* start automatically on demand even if you did not call this method.
*
* @throws IllegalStateException if this timer has been
* {@linkplain #stop() stopped} already
*/
public void start() {
switch (WORKER_STATE_UPDATER.get(this)) {
case WORKER_STATE_INIT:
if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
workerThread.start();
}
break;
case WORKER_STATE_STARTED:
break;
case WORKER_STATE_SHUTDOWN:
throw new IllegalStateException("cannot be started once stopped");
default:
throw new Error("Invalid WorkerState");
}
// Wait until the startTime is initialized by the worker.
while (startTime == 0) {
try {
startTimeInitialized.await();
} catch (InterruptedException ignore) {
// Ignore - it will be ready very soon.
}
}
}
@Override
public Set<Timeout> stop() {
if (Thread.currentThread() == workerThread) {
throw new IllegalStateException(View on GitHub (pinned to 3a3043227f)
Solutions
- Do not reuse a HashedWheelTimer after stop() — create a new instance if timer functionality is needed again.
- Ensure all components that reference the timer are stopped/unregistered before calling timer.stop().
- Guard scheduling calls with timer.isStop() before calling newTimeout() or start().
- Share a single long-lived HashedWheelTimer across the application (it is designed for this) rather than creating/stopping per-connection instances.
Example fix
// before — reusing timer after stop
timer.stop();
// ... later ...
timer.newTimeout(task, 5, TimeUnit.SECONDS); // calls start() internally -> throws
// after — check lifecycle before use
if (!timer.isStop()) {
timer.newTimeout(task, 5, TimeUnit.SECONDS);
} else {
timer = new HashedWheelTimer(); // new instance
timer.newTimeout(task, 5, TimeUnit.SECONDS);
} Defensive patterns
Strategy: validation
Validate before calling
if (!timer.isStop()) {
timer.start(); // or timer.newTimeout(...)
} else {
// timer is stopped — create a new instance or skip
timer = new HashedWheelTimer();
timer.start();
} Type guard
public boolean isTimerUsable(HashedWheelTimer timer) {
return timer != null && !timer.isStop();
} Try / catch
try {
timer.newTimeout(task, delay, unit);
} catch (IllegalStateException e) {
if (e.getMessage().contains("cannot be started once stopped")) {
logger.warn("Timer was stopped, replacing instance");
timer = new HashedWheelTimer();
timer.newTimeout(task, delay, unit);
} else {
throw e;
}
} Prevention
- Never reuse a HashedWheelTimer after stop() — create a new instance.
- Check timer.isStop() before scheduling new timeouts.
- Share a single long-lived timer across the application; stop it only at final shutdown.
- Track timer lifecycle with a reference holder that nulls out the reference after stop().
When it happens
Trigger: Calling start() (directly or indirectly via newTimeout()) on a HashedWheelTimer instance that has already had stop() called. This commonly happens when timer instances are reused or pooled and a stopped instance is accidentally used again.
Common situations: Reusing a HashedWheelTimer after calling stop(); framework shutdown that stops the shared timer followed by a late task scheduling attempt; connection/channel pools that cache and reuse timer references past their lifecycle; cleanup ordering where the timer is stopped before all users are quiesced.
Related errors
- HashedWheelTimer.stop() cannot be called from TimerTask
- ScopeBeanFactory is destroyed
- ExtensionDirector is destroyed
- ExtensionLoader is destroyed: ${type}
- Executor is shutdown!
AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14).
Data as JSON: /api/errors/3b8c087ba4210b4f.
Report an issue: GitHub.