alibaba/spring-ai-alibaba · error · IllegalStateException

Thread is not active or already released: <threadName>

Error message

Thread is not active or already released: <threadName>

What it means

During release(), MongoSaver marks the thread as released with a conditional findOneAndUpdate filtered on _id = metaId AND is_released = false. If the update matches nothing (thread already released, or meta vanished between the read and the update), the transaction is aborted and IllegalStateException('Thread is not active or already released: <threadName>') is thrown.

Source

Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/mongo/MongoSaver.java:490

				throw new IllegalStateException("Thread not found: " + threadName);
			}

			// Mark thread as released atomically
			// Use findOneAndUpdate with condition to ensure we only release active threads
			Document releaseFilter = new Document("_id", metaId)
					.append(FIELD_IS_RELEASED, false); // Only release if not already released

			Document updatedDoc = threadMetaCollection.findOneAndUpdate(
					clientSession,
					releaseFilter,
					Updates.set(FIELD_IS_RELEASED, true),
					new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER)
			);

			if (updatedDoc == null) {
				// Thread was already released or doesn't exist
				clientSession.abortTransaction();
				throw new IllegalStateException("Thread is not active or already released: " + threadName);
			}

			// Get checkpoints for Tag (using thread_id)
			MongoCollection<Document> checkpointCollection = database.getCollection(CHECKPOINT_COLLECTION);
			String checkpointDocId = CHECKPOINT_PREFIX + threadId;
			Document checkpointDoc = checkpointCollection.find(clientSession, new BasicDBObject("_id", checkpointDocId))
					.first();

			Collection<Checkpoint> checkpoints = Collections.emptyList();
			if (checkpointDoc != null) {
				String checkpointsStr = checkpointDoc.getString(DOCUMENT_CONTENT_KEY);
				if (checkpointsStr != null) {
					checkpoints = deserializeCheckpoints(checkpointsStr);
				}
			}

			clientSession.commitTransaction();
			return new Tag(threadName, checkpoints);

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Treat release as non-idempotent: track released threads in application state and skip already-released ones
  2. Catch IllegalStateException and, if the message says 'already released', treat it as success in cleanup paths
  3. Serialize releases per thread (locking or a leader election) if multiple workers may release concurrently

Example fix

// before
saver.release(cfg); // second call throws
// after
try {
    saver.release(cfg);
} catch (IllegalStateException e) {
    if (!e.getMessage().contains("already released")) throw e;
    // already released: treat as success
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    saver.release(cfg);
} catch (IllegalStateException e) {
    if (e.getMessage() != null && e.getMessage().contains("already released")) {
        return; // idempotent success
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling release(config) twice on the same thread; concurrent release calls racing on the same threadId; a thread that was released in a previous run but is still referenced by application state.

Common situations: Retry logic re-invoking release after a timeout even though the first attempt committed; multiple workers cleaning up the same thread; idempotency assumptions in batch shutdown code.

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/aa8579d637173681. Report an issue: GitHub.