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

  1. Do not reuse a HashedWheelTimer after stop() — create a new instance if timer functionality is needed again.
  2. Ensure all components that reference the timer are stopped/unregistered before calling timer.stop().
  3. Guard scheduling calls with timer.isStop() before calling newTimeout() or start().
  4. 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

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


AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14). Data as JSON: /api/errors/3b8c087ba4210b4f. Report an issue: GitHub.