alibaba/spring-ai-alibaba · error · IllegalStateException

ConfigAgentWatcher is already started

Error message

ConfigAgentWatcher is already started

What it means

ConfigAgentWatcher.start() throws IllegalStateException when invoked on a watcher instance whose `started` flag is already true. A watcher schedules a repeating file-check task and must only be started once per instance. Starting it again would leak duplicate scheduled tasks that each fire change callbacks.

Source

Thrown at spring-ai-alibaba-studio/src/main/java/com/alibaba/cloud/ai/agent/studio/loader/ConfigAgentWatcher.java:56

 * <p>The watcher polls for changes at regular intervals rather than using native filesystem events
 * for better cross-platform compatibility.
 */
class ConfigAgentWatcher {
	private static final Logger logger = LoggerFactory.getLogger(ConfigAgentWatcher.class);

	private final Map<Path, ChangeCallback> watchedFolders = new ConcurrentHashMap<>();
	private final Map<Path, Map<Path, Long>> watchedYamlFiles = new ConcurrentHashMap<>();
	private final ScheduledExecutorService fileWatcher = Executors.newSingleThreadScheduledExecutor();
	private volatile boolean started = false;

	/**
	 * Starts watching for file changes.
	 *
	 * @throws IllegalStateException if the watcher is already started
	 */
	synchronized void start() {
		if (started) {
			throw new IllegalStateException("ConfigAgentWatcher is already started");
		}

		logger.info("Starting ConfigAgentWatcher");
		fileWatcher.scheduleAtFixedRate(this::checkForChanges, 2, 2, TimeUnit.SECONDS);
		started = true;

		Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
		logger.info(
				"ConfigAgentWatcher started successfully. Watching {} folders.", watchedFolders.size());
	}

	/** Stops the file watcher. */
	synchronized void stop() {
		if (!started) {
			return;
		}

		logger.info("Stopping ConfigAgentWatcher...");

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Guard the call: only invoke start() if the watcher has not been started (track a boolean or check the watcher's state)
  2. Create a fresh ConfigAgentWatcher instance instead of restarting the old one
  3. Consolidate bootstrap logic so start() is called from exactly one lifecycle hook
  4. Catch IllegalStateException around start() if double-start is benign in your flow

Example fix

// before
watcher.start(); // called from @PostConstruct and again from onApplicationEvent
// after
if (!watcherStarted.getAndSet(true)) {
    watcher.start();
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (watcherStarted) { throw new IllegalStateException("watcher already started"); }

Try / catch

try {
    watcher.start();
} catch (IllegalStateException alreadyStarted) {
    logger.debug("Watcher already running; ignoring duplicate start");
}

Prevention

When it happens

Trigger: Calling start() twice on the same ConfigAgentWatcher instance — e.g. calling start() in both a @PostConstruct hook and application-ready listener, restarting the app lifecycle without creating a new watcher, or a retry path re-invoking start().

Common situations: Spring bean lifecycle callbacks firing start() more than once; manual re-init code reusing an old watcher object; copy-pasted bootstrap code calling start() in multiple configuration classes.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/82f1aeb2fd39d3b9. Report an issue: GitHub.